package fem2;

import inf.math.LinAlg;

import java.util.ArrayList;

import math2.MathUtilities;
import fem2.enu.NeighbourCriteria;
import fem2.enu.SortingCriteria;
import fem2.mesh_and_topology.SpatialInfo;

/**
 * 
 * The <code>Node</code> class provides the method to return the position of a
 * node.
 * 
 * @author hbui
 */
public class Node extends MeshPart implements SpatialInfo {

	private double[] position;
	private Constraint c;
	private int nModes = 1;

	/**
	 * create 2d node
	 * 
	 * @param x1
	 * @param x2
	 */
	public Node(double x1, double x2) {
		this.position = new double[] { x1, x2 };
	}

	/**
	 * create 3d node
	 * 
	 * @param x1
	 * @param x2
	 * @param x3
	 */
	public Node(double x1, double x2, double x3) {
		this.position = new double[] { x1, x2, x3 };
	}

	/**
	 * create node in n-dimesional array
	 * 
	 * @param x
	 */
	public Node(double... x) {
		this.position = x;
	}

	/**
	 * set number of modes associated with this node. Default nModes = 1
	 * 
	 * @param nModes
	 */
	public void setNumberOfModes(int nModes) {
		this.nModes = nModes;
	}

	@Override
	public int countModes() {
		return nModes;
	}

	@Override
	public Node[] getNodes() {
		return new Node[] { this };
	}

	/**
	 * 
	 * @return the position of the node
	 */
	@Override
	public double[] getPosition() {
		return this.position;
	}

	/**
	 * Set the constraint.
	 * 
	 * @param constraint
	 */
	public final void setConstraint(Constraint constraint) {
		this.c = constraint;
	}

	/**
	 * add the constraint to this node. If the node have no constraint, it will
	 * be assigned the constraint. If the node already has a constraint, new
	 * constraint will be formed by added with existing constraint
	 * 
	 * @param c
	 */
	public void addConstraint(Constraint c) {
		if (this.c == null) {
			this.c = c;
		} else {
			this.c = (Constraint) this.c.clone();
			this.c.add(c);
		}
	}

	/**
	 * Get the constraint.
	 * 
	 * @return constraint associated with mesh part
	 */
	public final Constraint getConstraint() {
		return this.c;
	}

	@Override
	public void updateSolution() {
	}

	/**
	 * Enumerate the dofs for the node
	 * 
	 * @param idx
	 *            initial indexing for the mesh part
	 * @return the last index number
	 */
	public final int enumerateDofs(int idx) {
		int nModes = countModes();
		this.dofNumbers = new int[this.d * nModes];
		int cnt = idx;
		for (int i = 0; i < this.d; i++) {
			if (this.c != null) {
				for (int j = 0; j < nModes; j++) {
					this.dofNumbers[j * this.d + i] = this.c.isFree(i) ? cnt++ : -1;
				}
			} else {
				for (int j = 0; j < nModes; j++) {
					this.dofNumbers[i * nModes + j] = cnt++;
				}
			}
		}
		return cnt;
	}

	@Override
	public ArrayList<MeshPart> getNeighbours(Mesh mesh, NeighbourCriteria criteria) {
		ArrayList<MeshPart> mps = new ArrayList<MeshPart>();

		if (criteria == NeighbourCriteria.FACES_NEIGHBOUR_WITH_NODE) {
			for (int i = 0; i < mesh.countFaces(); i++) {
				Face f = mesh.getFace(i);
				Node[] N = f.getNodes();
				for (Node n : N) {
					if (n.equals(this)) {
						mps.add(f);
						break;
					}
				}
			}
			return mps;
		} else if (criteria == NeighbourCriteria.BLOCKS_NEIGHBOUR_WITH_NODE) {
			for (int i = 0; i < mesh.countBlocks(); i++) {
				Block b = mesh.getBlock(i);
				Node[] N = b.getNodes();
				for (Node n : N) {
					if (n.equals(this)) {
						mps.add(b);
						break;
					}
				}
			}
			return mps;
		} else if (criteria == NeighbourCriteria.EDGES_NEIGHBOUR_WITH_NODE) {
			for (int i = 0; i < mesh.countEdges(); i++) {
				Edge e = mesh.getEdge(i);
				Node[] N = e.getNodes();
				for (Node n : N) {
					if (n.equals(this)) {
						mps.add(e);
						break;
					}
				}
			}
			return mps;
		} else {
			/*
			 * TODO
			 */
			throw new NotImplementedError();
		}
	}

	@Override
	public double[][] jacXAt(double... xi) {
		throw new Error("jacobian for node is not existed");
	}

	@Override
	public double[][] gGradientsAt(double... xi) {
		throw new Error("gradients is not defined on node");
	}

	@Override
	public double[][] jacUAt(double... xi) {
		throw new Error("jacobian for node is not existed");
	}

	@Override
	public double[][] gJacUAt(double... xi) {
		throw new Error("gradients of solutions for node is not existed");
	}

	@Override
	public String toString() {
		String str = "[" + Integer.toString(getId()) + ": ";
		for (int i = 0; i < position.length - 1; i++) {
			str += position[i] + ", ";
		}
		str += position[position.length - 1] + "]";
		return str;
	}

	@Override
	public boolean equals(Object o) {
		/*
		 * check if o is the same reference to node
		 */
		if (this == o) {
			return true;
		}
		/*
		 * check if o is instance of Node
		 */
		if (!(o instanceof Node)) {
			return false;
		}
		/*
		 * check the length of node o
		 */
		double[] p = ((Node) o).getPosition();
		if (position.length != p.length) {
			return false;
		}
		/*
		 * check if the position is the same
		 */
		double d = LinAlg.diffNorm2(p.length, position, p);
		return d < MathUtilities.tol6;
	}

	@Override
	public int compareTo(MeshPart o) {
		if (this == o) {
			return 0;
		}
		if (!this.getClass().getName().equals(o.getClass().getName())) {
			throw new Error("can't compare ojects of different type of MeshPart");
		}
		if (getSortingCriteria() != o.getSortingCriteria()) {
			throw new Error("can't compare ojects of different sorting criteria");
		}
		if (getSortingCriteria() == SortingCriteria.SORT_ON_ID) {
			if (getId() > o.getId()) {
				return 1;
			} else {
				return -1;
			}
		} else if (getSortingCriteria() == SortingCriteria.SORT_ON_X_COORDINATE) {
			if (position[0] > ((Node) o).getPosition()[0]) {
				return 1;
			} else if (position[0] == ((Node) o).getPosition()[0]) {
				return 0;
			} else {
				return -1;
			}
		} else if (getSortingCriteria() == SortingCriteria.SORT_ON_Y_COORDINATE) {
			if (position[1] > ((Node) o).getPosition()[1]) {
				return 1;
			} else if (position[1] == ((Node) o).getPosition()[1]) {
				return 0;
			} else {
				return -1;
			}
		} else if (getSortingCriteria() == SortingCriteria.SORT_ON_Z_COORDINATE) {
			if (position[2] > ((Node) o).getPosition()[2]) {
				return 1;
			} else if (position[2] == ((Node) o).getPosition()[2]) {
				return 0;
			} else {
				return -1;
			}
		} else {
			throw new NotImplementedError();
		}
	}
}
